/**
 *
 */
package co.fitstart.payment.web.controller.wechat;

import java.io.StringReader;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.demo2do.core.wechat.pay.WechatPayNotification;
import com.demo2do.core.wechat.pay.WechatPayNotification.StreamReaderDelegate;

import co.fitstart.payment.Constant;
import co.fitstart.payment.cache.accessor.PersistentAccessor;
import co.fitstart.payment.entity.app.App;
import co.fitstart.payment.entity.app.AppSetting;
import co.fitstart.payment.entity.channel.Channel;
import co.fitstart.payment.entity.request.RequestRecord;
import co.fitstart.payment.entity.transaction.OperationType;
import co.fitstart.payment.entity.transaction.TransactionRecord;
import co.fitstart.payment.entity.transaction.TransactionStatus;
import co.fitstart.payment.service.AppService;
import co.fitstart.payment.service.ChannelService;
import co.fitstart.payment.service.RequestRecordService;
import co.fitstart.payment.service.TransactionRecordService;
import co.fitstart.payment.support.NotificationResult;
import co.fitstart.payment.web.handler.OrderHandler;
import co.fitstart.payment.web.handler.SignatureHandler;

/**
 * @author lute
 *
 */
@Controller
@RequestMapping("/wx")
public class WechatCallbackController {

    private static final Logger logger = LoggerFactory.getLogger(WechatCallbackController.class);

    private static final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();

    @Autowired
    private PersistentAccessor accessor;

    @Autowired
    private SignatureHandler signatureHandler;

    @Autowired
    private OrderHandler orderHandler;

    @Autowired
    private AppService appService;

    @Autowired
    private ChannelService channelService;

    @Autowired
    private RequestRecordService requestRecordService;

    @Autowired
    private TransactionRecordService transactionRecordService;

    @Autowired
    public void setJaxb2Marshaller(Jaxb2Marshaller jaxb2Marshaller) throws JAXBException {
        JAXBContext context = jaxb2Marshaller.getJaxbContext();
        this.unmarshaller = context.createUnmarshaller();
    }

    private Unmarshaller unmarshaller;

    /**
     * Response to WeChat payment notification
     *
     * @param httpEntity
     * @return
     */
    @RequestMapping(value = "/callback", method = RequestMethod.POST)
    public ModelAndView callback(HttpEntity<String> httpEntity) {

        logger.trace("WechatCallbackController#callback() - Receive notification response from WeChat, start to decrypt.");

        ModelAndView modelAndView = new ModelAndView("wechat-notification-response");

        // Get WeChat notification from http entity
        WechatPayNotification notification = null;
        try {
            XMLStreamReader streamReader = xmlInputFactory.createXMLStreamReader(new StringReader(httpEntity.getBody()));
            StreamReaderDelegate delegate = new WechatPayNotification.StreamReaderDelegate(streamReader);
            notification = unmarshaller.unmarshal(delegate, WechatPayNotification.class).getValue();
        } catch (Exception e) {
            logger.error("WechatCallbackController#callback() - Unmarshalling callback entity encounters exception: {}", e);
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // If return code is FAIL, no need to process subsequent logic, log error and return FAIL to WeChat
        if (notification == null || !notification.checkReturnCode()) {
            logger.error("WechatCallbackController#callback() - Revceive return code [{}] and return message [{}].", notification.getReturnCode(), notification.getReturnMsg());
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // If result code is FAIL, no need to process subsequent logic, log error and return FAIL to WeChat
        if (!notification.checkResultCode()) {
            logger.error("WechatCallbackController#callback() - Revceive result code [{}], error code [{}] and error code description [{}].", notification.getResultCode(), notification.getErrCode(), notification.getErrCodeDes());
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // ===========================================================================
        // Otherwise, notification is success. Then go to check notification validity.
        // ===========================================================================

        // Get request number
        String requestNo = notification.getOutTradeNo();
        logger.trace("WechatCallbackController#callback() - Get out trade number [" + requestNo + "] as the request number.");

        // Get request record according to request number
        RequestRecord requestRecord = requestRecordService.getRequestRecord(requestNo);

        // Get app
        App app = appService.load(requestRecord.getAppId());
        if (app == null) {
            logger.error("WechatCallbackController#callback() - No app found with id [{}].", requestRecord.getAppId());
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // Get WeChat pay secret setting
        Map<Long, Map<String, AppSetting>> settings = accessor.getAppSettings();
        AppSetting paySecretSetting = settings.get(app.getId()).get(Constant.SETTING_WX_PAYSCRECT);
        if (paySecretSetting == null) {
            logger.error("WechatCallbackController#callback() - No WeChat appsecret found from app settings with app id [{}].", app.getId());
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // If signature is invalid, return FAIL either.
        if (!signatureHandler.checkWechatNotificationSignature(notification, paySecretSetting.getValue())) {
            logger.error("WechatCallbackController#callback() - Signature checking fails...");
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            modelAndView.addObject("returnMsg", "签名校验失败");
            return modelAndView;
        }

        // ========================================================
        // Notification is success and signature checking passes...
        // ========================================================

        // Get transaction record
        TransactionRecord transactionRecord = transactionRecordService.getTransactionRecord(requestNo);

        // Create transaction record callback log for the first place
        if (transactionRecord != null) {
            transactionRecordService.createTransactionLog(transactionRecord, OperationType.PAYMENT_CALLBACK);
        }

        // Get the paid total fee and pay time
        int totalFee = Integer.valueOf(notification.getTotalFee()).intValue();
        String payTimeString = notification.getTimeEnd();

        // Load channel
        Channel channel = channelService.load(requestRecord.getChannelId());
        logger.trace("WechatCallbackController#callback() - Get total fee [{}] and time end [{}].", totalFee, payTimeString);

        // Go to check transaction record status. If is created go to sync order, if sync order success go to synchronize transaction record
        if(transactionRecord == null) {
            logger.error("WechatCallbackController#callback() - Synchronizing order status fails, there is no transactionRecord!");
            modelAndView.addObject("returnCode", NotificationResult.FAIL);
            return modelAndView;
        }

        // alerday paid, just return success
        if(transactionRecord.getTransactionStatus().isPaid()) {
            logger.trace("WechatCallbackController#callback() Synchronizing order success, transactionRecord is alerday paid status");
            modelAndView.addObject("returnCode", NotificationResult.SUCCESS);
            return modelAndView;
        }

        // created status, sync order first, then update transaction record
        if(transactionRecord.getTransactionStatus().isCreated()) {

            boolean isOrderSync = orderHandler.synchronizeOrderStatus(app, requestRecord, channel, totalFee, payTimeString);

            if (isOrderSync) {
                // Get transaction id from WeChat notification and update transaction record
                String transactionId = notification.getTransactionId();
                logger.trace("WechatCallbackController#callback() Synchronizing order success, transactionRecord is created status, update into paid status with transactionId [{}]", transactionId);
                transactionRecordService.changeTransactionStatus(transactionRecord, TransactionStatus.PAID, transactionId);

                modelAndView.addObject("returnCode", NotificationResult.SUCCESS);

            } else {

                logger.error("WechatCallbackController#callback() - Synchronizing order status fails, please check the log...");
                modelAndView.addObject("returnCode", NotificationResult.FAIL);
            }
        }

        return modelAndView;
    }

}
